9.1 函数的定义和调用
语法
1 | 返回类型 函数名(形式参数){ |
简析
组成 | 说明 |
---|---|
返回类型(return type) | 每次调用函数返回数据的类型,当不需要返回值时使用void |
形式参数(parameter) | 每一个参数都必须有类型,没有形参使用void |
函数体(body) | 用{} 括起来的执行部分 |
实际参数(argument) | 为了激活(即调用)函数,需要写出函数名及跟随其后的实际参数列表 |
9.1.1 程序:计算平均值
1 | /** |
1 | $ ./average |
9.1.2 程序:显示倒数计时
1 | /** |
1 | ./countdwn |
9.1.3 程序:显示双关语
1 | /** |
1 | $ ./pun2 |
9.1.4 函数定义
函数定义:
1 | 返回类型 函数名(形式参数){ |
返回值
规则:
- 函数无法返回数组,但是没有其它关于返回类型的限制
- 如果忽略返回类型,那么会假定函数返回值的类型是
int
型 - 指定返回类型是
void
型,说明函数没有返回值
技巧:
- 为每个函数指定一个明显的返回类型是一个很好的方法(
void
也可以省略,毕竟经典c没有void
概念,但不推荐这样) - 如果返回值很冗长,比如
unsigned long int
,那么把返回类型单独放一行是非常有用的
参数列表
规则:
- 需要在每个形式参数的前面说明其类型
- 形式参数间用逗号进行分隔
- 如果函数没有形式参数,那么在
()
中应该出现void
- 即使有些形参具有相同数据类型,也必须对每个形式参数分别进行类型说明
函数体
规则:
- 函数体内声明的变量专属于此函数,其它函数不能对这些变量进行检查或修改
- 函数体可以为空,以后可以回来编写它的函数体
9.1.5 函数调用
语法:[强制转换返回类型] 函数名(实参1, 实参2 ...);
有无返回值的函数调用的区别:void型的函数调用是语句,所以调用后边始终跟着分号;非void型的函数调用是表达式。
注意:丢掉圆括号仍然是合法的表达式,但不起任何作用,有些编译器会给出警告。
强制转换返回类型
说明:很少用到,可以省略
技巧:可以将返回值强制类型转换成void
,使别人清楚编写者使故意扔掉返回值的。
1 | (void) printf("Hi, Mom!\n"); |
9.1.6 程序:判定素数
1 | /** |
1 | $ ./prime |
9.2 函数声明
背景:c语言没有要求函数的定义必须放置在main
函数的定义之后。如果函数的定义在调用之后,那么编译器会为被调用的函数的函数做一些假设。如果假设错误,则程序无法正常工作。
说明:在调用前声明(declare)每个函数,函数声明是编译器对函数进行概要浏览,而函数的完整定义稍后再出现。
函数原型(function prototype):c中将这种函数声明称为函数原型。原型为如何调用函数提供了一个完整的描述:提供了多少实际参数,这些参数应该是什么类型,以及返回的结果是什么类型。
行参规则:可以不写形式参数的名字,只要显示类型就可以,float average(float float)
,但不建议,为了代码的可阅读性和可维护性。
语法:返回类型 函数名(形式参数);
注意:函数的声明必须和函数的定义一致。
9.3 实际参数
分类 | 说明 |
---|---|
形式参数(parameter) | 出现在函数定义中,它们以假名字来表示函数调用时提供的值 |
实际参数(argument) | 出现在函数调用中的表达式 |
通过值传递:调用函数时,计算出每个实际参数的值并且把它赋值给相应的形式参数。
特点:在函数的执行过程中,对形式参数的改变不会影响实际参数的值。
9.3.1 实际参数的转换
说明:c语言允许在实际参数的类型和形式参数的类型不匹配的情况下进行函数调用。
实参转换规则
编译器在调用前是否遇到原型 | 转换方式 |
---|---|
是 | 实参的值被隐式转换成相应形式参数的类型。 |
否 | 编译器执行默认的实际参数提升 |
默认的实际参数提升
- 把
float
型的实际参数转换成double
类型 - 执行整数的提升(即把
char
型和short
型的实际参数转换成int
型)
注意:默认的实际参数提升不总会获得期望的效果
技巧:始终在调用函数前声明函数非常必要。
1 | int main(){ |
9.3.2 数组型实际参数
语法:数组类型 数组实参名[数组长度]
特点:
- 其中
数组长度
通常省略不写。即便写了在函数中仍然无法判断数组的长度。 - 多维数组形式参数只能忽略第一位的长度
获取实参数组长度:c语言没有提供任何简便的方法来确定传递给它的数组的长度。但是如果函数需要,必须把长度作为额外的实际参数提供给函数。
注意:在函数内部通过sizeof(a) / sizeof(a[0])
的方式并不能正确计算出传递进来的数组的长度。
技巧:通过传递比数组实际长度小的整数参数,来部分操作数组。
延伸:不能传递令人困扰的具有任意列数的多维数组。幸运的是,我们经常可以通过使用指针数组的方式处理。
1 |
|
9.4 return语句
return语句:return 表达式;
隐式转换:如果return
语句中表达式的类型和函数的返回类型不匹配,那么系统将会把表达式的类型隐式转换为返回类型
立即返回:在返回类型为void
的函数中可以使用return;
,会导致函数立即返回,后面不能包含表达式
void
和非void
返回值类型是否为void |
是否必须return |
说明 |
---|---|---|
否 | 是 | 必须使用return 表达式 返回值,否则某些编译器会报错 |
是 | 否 | return 并不是必须的,因为在执行最后一条语句后函数将会自动跳转。 |
1 | void print_pun(void){ |
9.5 程序终止
状态码:即main
函数的返回值,默认为int
类型;或者exit
函数的实参。
状态码用途:在某些操作系统中程序终止时可以检测到状态码。
状态 | 状态码 |
---|---|
正常终止 | 0 |
异常终止 | 非0 |
技巧:即使不打算用状态码,确信每个c程序都返回状态码也是一个很好的实践,因为某些运行程序的人可能稍后再决定测试状态码。
exit函数
功能:终止程序,相当于在main
函数中使用return
。
实参:传递给exit
函数的实际参数和main
函数的返回值具有相同的含义,两者都说明程序终止时的状态。
所在库:<stdlib.h>
相关宏定义:EXIT_SUCCESS
和EXIT_FAILURE
,值由实现定义,典型的值是0和1
区别于return
:return
和exit
都可以在任何函数中调用,但return
只有在main
函数中调用才会终止程序;exit
在任何函数中调用都会终止程序。
技巧:一些程序员专门使用exit
函数以便于模式匹配程序可以很容易地定位程序中全部的退出点。
1 | exit(EXIT_SUCCESS); |
9.6 递归函数
说明:如果函数调用它本身,那么此函数就是递归的(recursice)。
技巧:为了防止无限递归,所有递归函数都需要某些类型的终止条件。
注意:c语言允许递归,但大多数c程序员并不经常使用递归。
1 | /** |
9.6.1 快速排序算法
分治法(divide-and-conquer):把一个大问题划分成多个较小的问题,然后采用相同的算法分别解决这些小问题。比如排序算法。
算法操作:
- 选择数组元素e(作为“分割元素”),然后重新排列数组使得元素从1一直到i-1都是小雨或等于元素e的,元素i包含e,而元素从i-1一直到n都是大于或等于e的。
- 通过递归地采用快速排序算法,对从1到i-1的元素进行排序。
- 通过递归地采用快速排序方法,对从i+1到n的元素进行排序。
自己实现
1 | /** |
9.6.2 程序:快速排序
1 | /** |
9.6.3 改进
改进分割算法
上面介绍的方法不是最有效的。我们不再选择数组中的第一个元素作为分割元素,较好的方法是取第一个元素、中间元素和最后一个元素的中间值。分割过程本身也可以加速。特别是,在两个while循环中避免测试low<high是可能的。采用不同的方法进行小数组排序
使得快速排序非递归
虽然快速排序本质上是使用递归算法,并且递归格式的快速排序是最容易理解的,但是实际上若去掉递归会更有效率。